package com.bluedot.www.framework.mvc.servlet;

import com.bluedot.www.core.common.HandwritingRuntimeException;
import com.bluedot.www.core.common.Pipe;
import com.bluedot.www.core.common.ResultCodeEnum;
import com.bluedot.www.core.pojo.DO.Log;
import com.bluedot.www.core.pojo.DTO.R;
import com.bluedot.www.core.pojo.DTO.UserDTO;
import com.bluedot.www.core.service.Service;
import com.bluedot.www.core.utils.DbUtil;
import com.bluedot.www.core.utils.IpUtil;
import com.bluedot.www.framework.mvc.servlet.http.HttpMessageConverter;
import com.bluedot.www.framework.mvc.servlet.http.ResponseEntity;
import com.bluedot.www.framework.mvc.utils.XmlUtil;
import org.apache.commons.fileupload.FileUploadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.sql.Timestamp;
import java.util.*;

/** <p>接受所有的请求，调用相应的service层方法进行处理</p>
 * 继承HttpServlet，重写init(ServletConfig),doGet(),doPost()
 * @author He Peng
 * @date 2021/8/3 16:59
 * @version 1.5
 */
public class MyDispatcherServlet extends HttpServlet{

    /**日志，用于记录特定系统或应用的消息*/
    Logger logger = LoggerFactory.getLogger(MyDispatcherServlet.class);


    private static String encode;

    /**管道，用于存储controller实例和方法 与url的对应关系*/
    private static Pipe pipe;


    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        logger.info("初始化MyDispatcherServlet");
        // 1. 加载配置文件，填充properties字段
        /*用于加载配置文件中的内容*/
        Properties properties = new Properties();
        doLoadConfig(config.getInitParameter("contextConfigLocation"), properties);

        // 获取service配置文件的位置
        String location = properties.getProperty("serviceLocation");
        encode = properties.getProperty("fileEncode");
        // servlet只区分这个url是属于那个类，然后将全限定类名 和 参数传递给url
        doMapping(location);

    }

    /**
     * 由服务器调用，用于处理http GET请求
     * @param req: 请求
     * @param resp: 响应
     * @author He Peng
     * @date 2021/8/7 19:37
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {

        logger.info("执行MyDispatcherServlet的doGet方法");
        try {
            // 设置响应数据格式
            resp.setCharacterEncoding("utf-8");
            resp.setContentType("application/json;charset=utf-8");

            Object result = beforeDispatch(req, resp);
//            Object result = dispatch(req, resp);

            if (result instanceof ResponseEntity) {
                // 上传文件至前端
                uploadFile((ResponseEntity) result, resp);
                return;
            }
            // 封装返回数据
            R r = wrapperResponseResult(result);
            // 在一个请求中，不能同时使用getOutputStream和getWriter两个流！
            resp.getWriter().write(HttpMessageConverter.toJson(r));
        } catch (Exception e) {
            e.printStackTrace();
            R r = exceptionHandler(e);
            try {
                resp.getWriter().write(HttpMessageConverter.toJson(r));
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }





    /**
     * 由服务器调用，用于处理http GET请求
     * @param req: http请求
     * @param resp: http响应
     * @author He Peng
     * @date 2021/8/7 19:47
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        logger.info("执行MyDispatcherServlet的doPost()方法");

        try {
            resp.setCharacterEncoding("utf-8");
            doGet(req, resp);
        } catch (Exception e) {
            e.printStackTrace();
            R r = exceptionHandler(e);
            try {
                resp.getWriter().write(HttpMessageConverter.toJson(r));
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }


    /**
     * 记录操作日志
     * @param req: http请求
     * @param resp: http响应
     * @return java.lang.Object
     * @author He Peng
     * @date 2021/8/23 20:29
     */
    private Object beforeDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        //日志url
        String logUrl;
        String url = req.getRequestURI();

        String contextPath = req.getContextPath();
        // 截取url
        url=url.replace(contextPath, "").replaceAll("/+", "/");
        logUrl = url;
        // 去掉url前面的斜杠"/"，所有的@MyRequestMapping可以不用写斜杠"/"
        if (url.startsWith("/")) {
            url = url.substring(1);
        }
        if (url.endsWith("/")) {
            url = url.substring(0, url.length() - 1);
        }

        // 管道中不存在此方法
        Method method = pipe.getServiceMethod(url);
        if(method == null){
            String errorMsg = "";
            logger.info("404 NOT FOUND!---{}", url);
            return null;
        }

        //状态码
        int status = resp.getStatus();

        //ip 将string类型的ip转换为int
        //0:0:0:0:0:0:0:1
        String ipv4 = req.getRemoteAddr();

        //将本机localhost改为127.0.0.1
        if ("0:0:0:0:0:0:0:1".equals(ipv4)){
            ipv4 = "127.0.0.1";
        }

        Long intIp = IpUtil.ipv4ToInt(ipv4);

        //开始时间
        Date visitTime = new Date();
        //操作时间也是创建时间
        Timestamp gmtCreateTime = new Timestamp(System.currentTimeMillis());

//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm");
//        String format = simpleDateFormat.format(visitTime);

        Log log = new Log();

        log.setId(DbUtil.generateId());
        log.setIpAddress(intIp);
        log.setUrl(logUrl);
        log.setStatusCode(String.valueOf(status));
        log.setGmtCreate(gmtCreateTime);
        log.setGmtModified(gmtCreateTime);

        //执行分发请求
        Object dispatchResult = dispatch(req, resp);

        Date endTime = new Date();
        //访问时长
        Integer integerTime = Math.toIntExact(endTime.getTime() - visitTime.getTime());
        log.setResponseTime(integerTime);
//        System.out.println("响应时间=="+integerTime);

        if ("logout".equals(method.getName())) {
            log.setAccount(null);
            log.setUserId(null);
        } else {
            UserDTO userInfo = (UserDTO) req.getSession().getAttribute("SESSION_KEY_USER");
            String account = null;
            Long userId = null;
            if(userInfo!=null){
                account = userInfo.getUser().getAccount();
                userId = userInfo.getUser().getId();
            }
            log.setAccount(account);
            log.setUserId(userId);
        }
        Class<?> logServiceClass = Class.forName("com.bluedot.www.core.service.LogService");
        Object o = logServiceClass.getConstructor().newInstance();
        Method insertLogMethod = logServiceClass.getDeclaredMethod("insertLog", Log.class);
        insertLogMethod.setAccessible(true);
        Object res = insertLogMethod.invoke(o, log);

        return dispatchResult;
    }

    /**
     * 解决跨域问题
     * @param resp: Http 响应
     * @author He Peng
     * @date 2021/9/15 19:32
     */
    private void crossOrigin(HttpServletRequest req, HttpServletResponse resp) {
        //  这里最好明确的写允许的域名
        resp.setHeader("Access-Control-Allow-Origin", "*");
        resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
        resp.setHeader("Access-Control-Max-Age", "3600");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization,ybg");
        // Header 中携带 sessionId
        resp.setHeader("Access-Control-Expose-Headers", "TK");
    }

    /**
     * 分发请求
     * @param req: 请求
     * @param resp: 响应
     * @return java.lang.Object
     * @author He Peng
     * @date 2021/8/17 15:34
     */
    private Object dispatch(HttpServletRequest req, HttpServletResponse resp) throws ClassNotFoundException, IOException, FileUploadException {

        crossOrigin(req, resp);

        String url = req.getRequestURI();
        String contextPath = req.getContextPath();

        // 截取url
        url=url.replace(contextPath, "").replaceAll("/+", "/");
        // 去掉url前面的斜杠"/"，所有的@MyRequestMapping可以不用写斜杠"/"
        if (url.startsWith("/")) {
            url = url.substring(1);
        }
        if (url.endsWith("/")) {
            url = url.substring(0, url.length() - 1);
        }

        Method method = pipe.getServiceMethod(url);

        if(method == null){
            logger.info("404 NOT FOUND!---{}", url);
            return null;
        }


        // 获取方法的参数列表
        HttpMessageConverter httpMessageConverter = new HttpMessageConverter();
        Object[] paramValues = httpMessageConverter.getParamFromHttpMsg(method, req, resp);
        // 处理请求调用相应的方法，获取返回值
        Object result = null;

        String className = pipe.getServiceObject(url).getClass().getSimpleName();
        String methodName = pipe.getServiceMethod(url).getName();

        //第一个参数是method所对应的实例,根据url在controllerMap中找到; 第二个为前端传过来的参数
        return Service.doService(className, methodName, paramValues);
    }


        /**
     * 根据配置文件位置，读取配置文件中的配置信息，将其填充到properties字段
     * @param location: 配制文件位置
     * @author He Peng
     * @date 2021/8/7 19:56
     */
    private void doLoadConfig(String location, Properties properties) {
        // 对location参数做处理，如果配置文件中出现了"classpath:"则置为空
        if (location.startsWith("classpath:")) {
            location = location.replace("classpath:", "").replace("*", "");
            // 截取最后出现的 / 之后的内容
        } else if(location.contains("/")){
            int lastSplitIndex = location.lastIndexOf('/');
            location = location.substring(lastSplitIndex+1);
        }


        // 把web.xml中的contextConfigLocation对应value文件加载到流里面
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(location);

        logger.info("读取" + location + "里面的文件");
        try {
            // 使用Properties加载文件中的内容，读取键和元素列表
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // java中有boolean型，这样写的意义不大，如果其他语言写可以用这种写法，可以避免手滑写成 a = null，导致出错，效率上没有什么区别
            if (null != resourceAsStream) {
                try {
                    // 关闭输入流
                    resourceAsStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 初始化管道
     * @param handlerMapping:
     * @param serviceMapping:
     * @author He Peng
     * @date 2021/9/12 14:44
     */
    private void initPipe(Map<String, Method> handlerMapping, Map<String, Object> serviceMapping) {
        pipe = new Pipe(handlerMapping, serviceMapping);
    }


    /**
     * 上传文件至前端
     * @param responseEntity:
     * @param resp:
     * @author He Peng
     * @date 2021/9/12 15:44
     */
    private void uploadFile(ResponseEntity responseEntity, HttpServletResponse resp) {
        resp.setContentType("text/html;charset=utf-8");
        // 获取文件名
        String filename = responseEntity.getFileName();
        //设置文件名的编码
        try {
            filename= URLEncoder.encode(filename, encode);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        // 通知浏览器以下载的方式打开
        resp.addHeader("Content-Type","application/octet-stream");
        resp.addHeader("Content-Disposition","attachment;filename="+filename);
        // 通过文件输入流读取文件
        OutputStream out = null;
        try {
            out = resp.getOutputStream();
            out.write(responseEntity.getFileBytes());

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 基于xml的方式建立url和方法的映射关系
     * @author He Peng
     * @date 2021/8/13 18:25
     */
    private void doMapping(String location) {
        // 获取xml中的信息
        XmlUtil xmlUtil = new XmlUtil(location);
        Map<String, Map<String, String>> urlMethodMapping = XmlUtil.getUrlMethodMapping();
        List<String> classNames = new ArrayList<>(urlMethodMapping.keySet());


        Map<String, Object> ioc = new HashMap<>();

        /*存储Url和Controller中方法的对应关系*/
        Map<String, Method> handlerMapping = new  HashMap<>(60);

        /*存储Service实例*/
        Map<String, Object> serviceMapping = new HashMap<>(20);

        for (String className : classNames) {
            // 使用反射，根据类名实例化类
            try {
                Class<?> clazz = Class.forName(className);

                if (clazz.isInterface()) {
                    logger.warn("service层没有接口");
                }
                // 通过构造器 获得实例化对象
                Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
                declaredConstructor.setAccessible(true);
                ioc.put(toLowerFirstLetter(clazz.getSimpleName()), declaredConstructor.newInstance());
            } catch (ClassNotFoundException | IllegalAccessException |
                    InstantiationException | NoSuchMethodException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        try {
            for (Map.Entry<String, Object> entry: ioc.entrySet()) {
                Class<?> clazz = entry.getValue().getClass();

                Method[] methods = clazz.getDeclaredMethods();
                Map<String, String> urlMethod = urlMethodMapping.get(clazz.getName());
                for (Method method : methods) {
                    // 获取xml中的url
                    String url = urlMethod.get(method.getName());
                    if (null == url) {
                        // xml中不存在此方法
                        logger.warn("xml文件不包含【{}】", method.getName());
                        continue;
                    }
                    // url和service的方法的映射关系
                    handlerMapping.put(url,method);
                    // url和service实例的映射关系关系
                    serviceMapping.put(url, entry.getValue());
                }
            }

            // 初始化Pipe
            initPipe(handlerMapping, serviceMapping);

        } catch (Exception e) {
            e.printStackTrace();
            logger.error(e.getMessage(), e);
        }
    }



    /**
     * 全局异常处理，根据不同的异常类型，返回不同的异常信息至前端
     * @param e: 底层抛出的错误
     * @return com.bluedot.www.core.pojo.DTO.R
     * @author He Peng
     * @date 2021/8/24 10:41
     */
    private R exceptionHandler(Exception e) {
        if (e instanceof NullPointerException) {
            return R.setResult(ResultCodeEnum.NULL_POINT).message(e.getMessage());
        }
        else if (e instanceof ClassNotFoundException) {
            return R.setResult(ResultCodeEnum.PARAM_ERROR).message(e.getMessage());
        }
        else if (e instanceof HandwritingRuntimeException) {
            return R.error().message(e.getMessage());
        }
        else {
            return R.setResult(ResultCodeEnum.UNKNOWN_ERROR).message(e.getMessage());
        }
    }



    /**
     * 封装返回结果
     * @param result: Object，service层返回结果
     * @return com.bluedot.www.core.pojo.DTO.R
     * @author He Peng
     * @date 2021/8/23 20:36
     */
    private R wrapperResponseResult(Object result) {
        if (result == null) {
            return R.error().message("未获取到数据");
        } else if (result instanceof Integer) {
            if ((Integer) result >= 0) {
                return R.ok().data("item", result);
            } else {
                return R.error().message("操作失败");
            }
        }
        else if (result instanceof Boolean) {
            return (Boolean) result ? R.ok().message("操作成功，返回结果为 true") : R.error().message("操作失败，返回结果为 false");
        } else if ("".equals((result))) {
            return R.error().message("操作失败，未获取到数据");
        } else if (result instanceof List) {
            return R.ok().data("list", result);
        } else {
            return R.ok().data("item", result);
        }
    }


    /** 将字符串的首字母小写
     * @param str: 字符串
     * @return java.lang.String
     * @author Jiutwo
     * @date 2021/8/4 9:01
     */
    private String toLowerFirstLetter(String str) {
        char[] charArray = str.toCharArray();
        // 如果首字母是大写，则变为小写
        if (charArray[0] >= 'A' && charArray[0] <= 'Z') {
            charArray[0] += 32;
        }
        return String.valueOf(charArray);
    }


}
